namespace FluentDataContract
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;

    public sealed class ObjectBuilder<TObject> : IObjectBuilder<TObject>
    {
        private IObjectContract _contract;
        public IObjectContract Contract { get { return _contract ?? (_contract = new ObjectContract {Type = typeof(TObject)}); } }

        public IObjectBuilder<TObject> IncludeDefaultValues()
        {
            Contract.IncludeDefaultValues = true;

            return this;
        }

        public IObjectBuilder<TObject> ExcludeDefaultValues()
        {
            return this;
        }

        public IObjectBuilder<TObject> IncludeNullValues()
        {
            return this;
        }

        public IObjectBuilder<TObject> ExcludeNullValues()
        {
            return this;
        }

        // Public only!
        public IPropertyBuilder<TObject> IncludeAllProperties()
        {
            var members = Contract.Type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            var propertyBuilder = new PropertyBuilder<TObject>(this, members.Cast<MemberInfo>().ToArray());
            foreach(var contract in propertyBuilder.Contracts)
                Contract.Properties.Add(contract);

            return propertyBuilder;
        }

        public IPropertyBuilder<TObject> IncludeProperty<TProperty>(Expression<Func<TObject, TProperty>> expression)
        {
            var memberInfo = GetMemberForExpression(expression);
            var propertyBuilder = new PropertyBuilder<TObject>(this, memberInfo);

            Contract.Properties.Add(propertyBuilder.Contracts[0]);

            return propertyBuilder;
        }

        public IPropertyBuilder<TObject> IncludeProperties(params Expression<Func<TObject, object>>[] expressions)
        {
            var propertyBuilder = new PropertyBuilder<TObject>(this, expressions.Select(GetMemberForExpression).ToArray());

            foreach (var contract in propertyBuilder.Contracts)
                Contract.Properties.Add(contract);

            return propertyBuilder;
        }

        public IObjectBuilder<TObject> ExcludeProperty<TProperty>(Expression<Func<TObject, TProperty>> expression)
        {
            var memberInfo = GetMemberForExpression(expression);

            var found = Contract.Properties.Where(o => o.PropertyName == memberInfo.Name).ToList();
            if (found.Count > 0)
                Contract.Properties.Remove(found.First());
            
            return this;
        }

        public IObjectBuilder<TObject> ExcludeProperties(params Expression<Func<TObject, object>>[] expressions)
        {
            foreach(var expression in expressions)
                ExcludeProperty(expression);

            return this;
        }

        public IPropertyBuilder<TObject> Property<TProperty>(Expression<Func<TObject, TProperty>> expression)
        {
            return IncludeProperty(expression);
        }

        public IPropertyBuilder<TObject> Properties(params Expression<Func<TObject, object>>[] expressions)
        {
            return IncludeProperties(expressions);
        }

        private static MemberInfo GetMemberForExpression<TProperty>(Expression<Func<TObject, TProperty>> expression)
        {
            var expressionBody = expression.Body;
            var memberExpression =
                expressionBody is UnaryExpression
                ? ((UnaryExpression)expressionBody).Operand as MemberExpression
                : expressionBody as MemberExpression;

            if (memberExpression == null)
                throw new InvalidCastException("Expression does not resolve to a property.");

            return memberExpression.Member;
        }
    }
}